Skip to content

add built-in herdr integration - #360

Merged
kevinjosethomas merged 12 commits into
mainfrom
builtin-herdr-integration
Jul 9, 2026
Merged

add built-in herdr integration#360
kevinjosethomas merged 12 commits into
mainfrom
builtin-herdr-integration

Conversation

@kevinjosethomas

@kevinjosethomas kevinjosethomas commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Herdr does not detect Prime Agent panes today, for two reasons:

  1. Herdr's process-name detection only knows the legacy pi binary name, not prime-agent (upstream PR Add Prime Agent support for Pi integration herdrdev/herdr#822 to add the alias was auto-closed unmerged).
  2. herdr integration install pi writes its lifecycle-reporter extension to ~/.pi/agent/extensions/, which Prime Agent does not read (~/.prime/agent/).

This ships the lifecycle reporter as a built-in extension factory loaded for every session, so Prime Agent works inside Herdr panes out of the box with no manual install step and no dependency on upstream Herdr changes.

Behavior

  • Reports working / idle / blocked plus session file references to Herdr's unix socket via pane.report_agent, and releases the pane on session_shutdown.
  • Reports agent label prime-agent. Verified live against herdr 0.7.0: the label is accepted, and the pane shows "agent": "prime-agent" in herdr pane list / herdr agent list with correct working/idle transitions and release.
  • No-op unless HERDR_ENV=1 with HERDR_SOCKET_PATH and HERDR_PANE_ID set, so nothing changes outside Herdr.
  • Defers to Herdr's own file-based integration when herdr-agent-state.ts exists in the extensions dir (either ~/.prime/agent/ or legacy ~/.pi/agent/), so the two reporters never race on one pane.
  • All Herdr env capture happens inside the factory (invoked per session load), not at module scope: the module is statically imported and evaluated once per daemon process, so module-scope capture would pin every session to the daemon's startup pane. Factory-scope capture runs inside the daemon's per-session client-env window from scope client env to sessions so herdr extensions see the right pane #303 and picks up the right pane identity.

Testing

  • test/herdr-agent-state.test.ts: fake Herdr socket server asserting the report/release protocol, agent label, session refs, and both no-op paths (outside Herdr; file-based integration present).
  • Live protocol verification against the running herdr 0.7.0 server on this machine (report as prime-agent, state transitions, release).
  • npm run check passes.

Note

Medium Risk
Touches daemon session lifecycle and external Herdr IPC on every session load; logic is intricate but gated off Herdr env and heavily tested.

Overview
Adds a built-in Herdr integration so Prime Agent reports working / idle / blocked (and session file refs) to Herdr’s Unix socket as prime-agent, without herdr integration install pi.

The reporter is registered as a default inline extension factory in createAgentSessionServices. It is a no-op unless HERDR_ENV=1 with socket and pane id; it defers when a loaded file extension named herdr-agent-state.ts/js is active (via new getLoadedExtensionPaths()), and is disabled for RLM subagents (noBuiltinHerdrReporter when rlmDepth > 0) and when noExtensions is set.

The implementation handles queued socket writes, monotonic seq across reloads, retry/error hold before blocked, idle debounce when follow-ups are queued, parent-only binding for inline RLM children, release only on quit (not on session replace/reload), and herdr:blocked bus events. Covered by herdr-agent-state.test.ts.

Reviewed by Cursor Bugbot for commit 07bf7bd. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add built-in Herdr integration to report agent lifecycle state

  • Adds a built-in Herdr extension in herdr-agent-state.ts that connects via Unix socket when HERDR_ENV=1 and reports working/blocked/idle states to the configured Herdr pane.
  • The extension is injected automatically into createAgentSessionServices without requiring manual installation; it defers to any file-based Herdr integration that loaded in the same cycle.
  • Subagent runtimes (rlmDepth > 0) opt out via noBuiltinHerdrReporter=true to avoid racing or prematurely releasing the parent pane.
  • Includes debounced idle reporting, a retry grace window that holds working state across retryable errors, and a monotonically increasing sequence number for Herdr's per-source ordering requirement.
  • DefaultResourceLoader now exposes getLoadedExtensionPaths() so inline factories can inspect which file-based extensions loaded in the current cycle.

Macroscope summarized 07bf7bd.

Comment thread packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts Outdated
Comment thread packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts Outdated
@kevinjosethomas
kevinjosethomas force-pushed the builtin-herdr-integration branch from 47cb319 to 91223df Compare July 9, 2026 20:24
Comment thread packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts Outdated
Comment thread packages/coding-agent/src/core/agent-session-services.ts
@kevinjosethomas
kevinjosethomas force-pushed the builtin-herdr-integration branch from 2ab79ce to 9327ae8 Compare July 9, 2026 20:59
@kevinjosethomas
kevinjosethomas force-pushed the builtin-herdr-integration branch from b65debe to 0776578 Compare July 9, 2026 21:36
Herdr cannot detect Prime Agent panes: its process detection only knows
the legacy "pi" binary name, and its "herdr integration install pi"
writes to ~/.pi/agent/extensions which Prime Agent does not read.

Ship the pi lifecycle-reporter as a built-in extension factory loaded
for every session. It reports working/idle/blocked and session refs to
Herdr's unix socket and releases the pane on shutdown, using the agent
label "prime-agent" (verified accepted and displayed by herdr 0.7.0
against a live pane). The factory captures HERDR_* env per invocation,
so daemon sessions pick up their own pane identity inside the client-env
window, and it is a no-op outside Herdr or when the user has installed
Herdr's own file-based integration.
…se only on quit

Herdr guards pane.report_agent with a per-source monotonic seq and
silently drops lower-seq reports (verified live: ok response, state
unchanged). Seeding seq per factory instance meant a successor session
after /new, resume, fork, or reload could restart below the
predecessor's counter, so all its reports were dropped and the pane
stuck at working.

- move the seq counter to module scope and clamp it to
  max(prev + 1, now*1000) so it never regresses across instances
- release the pane only on quit; on session replacement the successor
  re-reports immediately and a racing release could clear it
- report idle immediately on agent_end when no messages are queued;
  keep the debounce only when queued follow-ups restart the loop
- unsubscribe the shared-bus herdr:blocked listener on session_shutdown;
  the event bus survives reloads and session replacements, so a leaked
  listener kept stale instances reporting with captured pane identity
- move the file-based integration check into createAgentSessionServices
  using the same extension dirs the loader discovers from, so agentDir
  overrides are honored
- stop treating the legacy ~/.pi/agent path as an active reporter;
  Prime Agent never loads extensions from there, so deferring to it left
  panes with no reporter at all
…k deferral on reload

- bind the reporter to the first session that starts and ignore events
  from other sessions sharing the closure; inline RLM children reuse the
  parent's resource loader, so a subagent's turns flipped the pane state
  and a subagent quit released the pane while the parent was running
- skip the built-in reporter entirely for subagent runtimes
  (rlmDepth > 0), which share the parent's HERDR_* pane identity
- re-check for Herdr's file-based integration on every factory
  invocation instead of once at service creation, so installing it and
  running /reload hands the pane over without both reporters active
- also match herdr-agent-state.js for compiled installs
releaseAgent now stops new reports, drops the queued state, and awaits
the in-flight drain before sending pane.release_agent, so a late report
cannot land after the release and reclaim the pane for an exited agent.
Daemon-driven reloads and extension ctx.reload() are not gated on idle,
so a fresh reporter's session_start could publish idle while the agent
was still streaming, and the agentActive guard then swallowed the
turn's real end transition. session_start now seeds agentActive from
ctx.isIdle().
… its timer

A herdr:blocked event during the retry-hold window cancelled the retry
timer but left retryHoldActive set, so after the block lifted the pane
reported working forever. The blocked handler now settles the hold to
failureBlocked, matching what the cancelled timer would have done.
@kevinjosethomas
kevinjosethomas force-pushed the builtin-herdr-integration branch from 0776578 to cb6d8f7 Compare July 9, 2026 21:48
…Extensions

With noExtensions the loader never scans the extension dirs, so a
file-based herdr-agent-state there never becomes an active reporter;
deferring to it left the pane with no reporter at all.
Comment thread packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts Outdated
Comment thread packages/coding-agent/src/core/agent-session-services.ts Outdated
let idleTimer: ReturnType<typeof setTimeout> | undefined;
let retryTimer: ReturnType<typeof setTimeout> | undefined;

function sendRequest(request: unknown): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium builtin/herdr-agent-state.ts:161

sendRequest() resolves on the first byte of response data or after a fixed 500ms timeout, so releaseAgent() can proceed to send pane.release_agent while an earlier pane.report_agent is still in-flight on the Herdr side. When Herdr is slow to process replies, the earlier report lands after the release and reclaims the pane, leaving Herdr showing prime-agent as attached after it has exited. The await activeDrain in releaseAgent() only guarantees the socket write completed, not that Herdr finished processing the report. Consider waiting for a full reply (or an explicit ack) before resolving sendRequest(), or draining pending reports with a stronger completion guarantee before sending the release.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts around line 161:

`sendRequest()` resolves on the first byte of response data or after a fixed 500ms timeout, so `releaseAgent()` can proceed to send `pane.release_agent` while an earlier `pane.report_agent` is still in-flight on the Herdr side. When Herdr is slow to process replies, the earlier report lands after the release and reclaims the pane, leaving Herdr showing `prime-agent` as attached after it has exited. The `await activeDrain` in `releaseAgent()` only guarantees the socket write completed, not that Herdr finished processing the report. Consider waiting for a full reply (or an explicit ack) before resolving `sendRequest()`, or draining pending reports with a stronger completion guarantee before sending the release.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining. Herdr's API handler applies the request before replying (its api.request.complete log lines follow the state change), so a pane.report_agent ACK means the report is already applied server-side. releaseAgent awaits the active drain — which resolves only on the report's ACK — before sending the release, so the release cannot be outrun by an already-ACKed report. The 500ms timeout path only fires when herdr never replies, in which case that connection is destroyed client-side and its late processing is the same failure mode as any dropped write; the released flag prevents any further reports after it.

…ern subset

The agent's auto-retry treats nearly all provider errors as retryable
and runs after extension agent_end, so an error the reporter's pattern
list missed flipped the pane to idle while a retry was pending. Drop
the pattern list and hold working for any error end; a retry's
agent_start keeps the pane working, and with no retry the grace window
settles it to blocked with the error message.
…utdown

A replaced instance kept its report queue armed after reload/new/resume
shutdowns; a late report carrying the process-wide seq could outrank
and stomp the successor's state. Non-quit shutdowns now set released
and drop the queued state so the old instance goes quiet.
noExtensions is a full opt-out: inline factories deliberately bypass it
for embedder-supplied extensions, but the built-in reporter should
respect it so --no-extensions leaves nothing extension-driven running.
…t loaded

Deferral now consults the resource loader's actually-loaded extension
paths (late-bound, per factory invocation) instead of raw disk
existence, so a herdr-agent-state file disabled via settings overrides
or outside the discovery dirs no longer silences the built-in reporter
and leaves the pane with no reporter.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 07bf7bd. Configure here.

// Set before inline factories run so a factory can see which file-based
// extensions actually loaded this cycle (e.g. the built-in Herdr reporter
// defers to Herdr's own file-based integration only when it is active).
this.loadedExtensionPaths = extensionPaths;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferral uses unloaded extension paths

Medium Severity

getLoadedExtensionPaths() is assigned the candidate extensionPaths list, not the extensions that successfully loaded. If herdr-agent-state.ts/js is discovered but fails to load, hasFileBasedHerdrIntegration still returns true and the built-in reporter no-ops, leaving the pane with no active Herdr reporter.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 07bf7bd. Configure here.

@kevinjosethomas
kevinjosethomas merged commit ecd3290 into main Jul 9, 2026
3 checks passed
@kevinjosethomas
kevinjosethomas deleted the builtin-herdr-integration branch July 16, 2026 23:51
zhengr pushed a commit to zhengr/prime-agent that referenced this pull request Aug 8, 2026
…f-windows

Fix edit tool failing on Windows due to CRLF line endings
zhengr pushed a commit to zhengr/prime-agent that referenced this pull request Aug 8, 2026
* feat(coding-agent): add built-in herdr integration

Herdr cannot detect Prime Agent panes: its process detection only knows
the legacy "pi" binary name, and its "herdr integration install pi"
writes to ~/.pi/agent/extensions which Prime Agent does not read.

Ship the pi lifecycle-reporter as a built-in extension factory loaded
for every session. It reports working/idle/blocked and session refs to
Herdr's unix socket and releases the pane on shutdown, using the agent
label "prime-agent" (verified accepted and displayed by herdr 0.7.0
against a live pane). The factory captures HERDR_* env per invocation,
so daemon sessions pick up their own pane identity inside the client-env
window, and it is a no-op outside Herdr or when the user has installed
Herdr's own file-based integration.

* fix(coding-agent): keep herdr seq monotonic across sessions and release only on quit

Herdr guards pane.report_agent with a per-source monotonic seq and
silently drops lower-seq reports (verified live: ok response, state
unchanged). Seeding seq per factory instance meant a successor session
after /new, resume, fork, or reload could restart below the
predecessor's counter, so all its reports were dropped and the pane
stuck at working.

- move the seq counter to module scope and clamp it to
  max(prev + 1, now*1000) so it never regresses across instances
- release the pane only on quit; on session replacement the successor
  re-reports immediately and a racing release could clear it
- report idle immediately on agent_end when no messages are queued;
  keep the debounce only when queued follow-ups restart the loop

* fix(coding-agent): address herdr integration review findings

- unsubscribe the shared-bus herdr:blocked listener on session_shutdown;
  the event bus survives reloads and session replacements, so a leaked
  listener kept stale instances reporting with captured pane identity
- move the file-based integration check into createAgentSessionServices
  using the same extension dirs the loader discovers from, so agentDir
  overrides are honored
- stop treating the legacy ~/.pi/agent path as an active reporter;
  Prime Agent never loads extensions from there, so deferring to it left
  panes with no reporter at all

* fix(coding-agent): scope herdr reporter to its own session and recheck deferral on reload

- bind the reporter to the first session that starts and ignore events
  from other sessions sharing the closure; inline RLM children reuse the
  parent's resource loader, so a subagent's turns flipped the pane state
  and a subagent quit released the pane while the parent was running
- skip the built-in reporter entirely for subagent runtimes
  (rlmDepth > 0), which share the parent's HERDR_* pane identity
- re-check for Herdr's file-based integration on every factory
  invocation instead of once at service creation, so installing it and
  running /reload hands the pane over without both reporters active
- also match herdr-agent-state.js for compiled installs

* fix(coding-agent): make quit release the final write to the herdr socket

releaseAgent now stops new reports, drops the queued state, and awaits
the in-flight drain before sending pane.release_agent, so a late report
cannot land after the release and reclaim the pane for an exited agent.

* fix(coding-agent): seed working state when the reporter starts mid-turn

Daemon-driven reloads and extension ctx.reload() are not gated on idle,
so a fresh reporter's session_start could publish idle while the agent
was still streaming, and the agentActive guard then swallowed the
turn's real end transition. session_start now seeds agentActive from
ctx.isIdle().

* fix(coding-agent): settle the retry hold when a blocked event cancels its timer

A herdr:blocked event during the retry-hold window cancelled the retry
timer but left retryHoldActive set, so after the block lifted the pane
reported working forever. The blocked handler now settles the hold to
failureBlocked, matching what the cancelled timer would have done.

* fix(coding-agent): do not defer to file-based herdr reporter under noExtensions

With noExtensions the loader never scans the extension dirs, so a
file-based herdr-agent-state there never becomes an active reporter;
deferring to it left the pane with no reporter at all.

* fix(coding-agent): hold working for every error end instead of a pattern subset

The agent's auto-retry treats nearly all provider errors as retryable
and runs after extension agent_end, so an error the reporter's pattern
list missed flipped the pane to idle while a retry was pending. Drop
the pattern list and hold working for any error end; a retry's
agent_start keeps the pane working, and with no retry the grace window
settles it to blocked with the error message.

* fix(coding-agent): silence replaced reporter instances on non-quit shutdown

A replaced instance kept its report queue armed after reload/new/resume
shutdowns; a late report carrying the process-wide seq could outrank
and stomp the successor's state. Non-quit shutdowns now set released
and drop the queued state so the old instance goes quiet.

* fix(coding-agent): make noExtensions disable the built-in herdr reporter

noExtensions is a full opt-out: inline factories deliberately bypass it
for embedder-supplied extensions, but the built-in reporter should
respect it so --no-extensions leaves nothing extension-driven running.

* fix(coding-agent): defer to the file-based herdr reporter only when it loaded

Deferral now consults the resource loader's actually-loaded extension
paths (late-bound, per factory invocation) instead of raw disk
existence, so a herdr-agent-state file disabled via settings overrides
or outside the discovery dirs no longer silences the built-in reporter
and leaves the pane with no reporter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant